/**
 * Copyright 2010 The Apache Software Foundation
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.wasp.util;

import com.alibaba.wasp.SQLErrorCode;
import com.alibaba.wasp.jdbc.JdbcException;
import com.alibaba.wasp.jdbc.value.Value;
import com.alibaba.wasp.jdbc.value.ValueDate;
import com.alibaba.wasp.jdbc.value.ValueNull;
import com.alibaba.wasp.jdbc.value.ValueTime;
import com.alibaba.wasp.jdbc.value.ValueTimestamp;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;


/**
 * This utility class contains time conversion functions.
 * <p>
 * Date value: a bit field with bits for the year, month, and day. Absolute day:
 * the day number (0 means 1970-01-01).
 */
public class DateTimeUtils {

  /**
   * The number of milliseconds per day.
   */
  public static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000L;

  private static final long NANOS_PER_DAY = MILLIS_PER_DAY * 1000000;

  private static final int SHIFT_YEAR = 9;
  private static final int SHIFT_MONTH = 5;

  private static final int[] NORMAL_DAYS_PER_MONTH = { 0, 31, 28, 31, 30, 31,
      30, 31, 31, 30, 31, 30, 31 };

  /**
   * Offsets of month within a year, starting with March, April,...
   */
  private static final int[] DAYS_OFFSET = { 0, 31, 61, 92, 122, 153, 184, 214,
      245, 275, 306, 337, 366 };

  private static int zoneOffset;
  private static Calendar cachedCalendar;

  private DateTimeUtils() {
    // utility class
  }

  static {
    getCalendar();
  }

  /**
   * Reset the calendar, for example after changing the default timezone.
   */
  public static void resetCalendar() {
    cachedCalendar = null;
    getCalendar();
  }

  private static Calendar getCalendar() {
    if (cachedCalendar == null) {
      cachedCalendar = Calendar.getInstance();
      zoneOffset = cachedCalendar.get(Calendar.ZONE_OFFSET);
    }
    return cachedCalendar;
  }

  /**
   * Convert the date to the specified time zone.
   * 
   * @param value
   *          the date (might be ValueNull)
   * @param calendar
   *          the calendar
   * @return the date using the correct time zone
   */
  public static Date convertDate(Value value, Calendar calendar) {
    if (value == ValueNull.INSTANCE) {
      return null;
    }
    ValueDate d = (ValueDate) value.convertTo(Value.DATE);
    Calendar cal = (Calendar) calendar.clone();
    cal.clear();
    cal.setLenient(true);
    long dateValue = d.getDateValue();
    setCalendarFields(cal, yearFromDateValue(dateValue),
        monthFromDateValue(dateValue), dayFromDateValue(dateValue), 0, 0, 0, 0);
    long ms = cal.getTimeInMillis();
    return new Date(ms);
  }

  /**
   * Convert the time to the specified time zone.
   * 
   * @param value
   *          the time (might be ValueNull)
   * @param calendar
   *          the calendar
   * @return the time using the correct time zone
   */
  public static Time convertTime(Value value, Calendar calendar) {
    if (value == ValueNull.INSTANCE) {
      return null;
    }
    ValueTime t = (ValueTime) value.convertTo(Value.TIME);
    Calendar cal = (Calendar) calendar.clone();
    cal.clear();
    cal.setLenient(true);
    long nanos = t.getNanos();
    long millis = nanos / 1000000;
    nanos -= millis * 1000000;
    long s = millis / 1000;
    millis -= s * 1000;
    long m = s / 60;
    s -= m * 60;
    long h = m / 60;
    m -= h * 60;
    setCalendarFields(cal, 1970, 1, 1, (int) h, (int) m, (int) s, (int) millis);
    long ms = cal.getTimeInMillis();
    return new Time(ms);
  }

  /**
   * Convert the timestamp to the specified time zone.
   * 
   * @param value
   *          the timestamp (might be ValueNull)
   * @param calendar
   *          the calendar
   * @return the timestamp using the correct time zone
   */
  public static Timestamp convertTimestamp(Value value, Calendar calendar) {
    if (value == ValueNull.INSTANCE) {
      return null;
    }
    ValueTimestamp ts = (ValueTimestamp) value.convertTo(Value.TIMESTAMP);
    Calendar cal = (Calendar) calendar.clone();
    cal.clear();
    cal.setLenient(true);
    long dateValue = ts.getDateValue();
    long nanos = ts.getNanos();
    long millis = nanos / 1000000;
    nanos -= millis * 1000000;
    long s = millis / 1000;
    millis -= s * 1000;
    long m = s / 60;
    s -= m * 60;
    long h = m / 60;
    m -= h * 60;
    setCalendarFields(cal, yearFromDateValue(dateValue),
        monthFromDateValue(dateValue), dayFromDateValue(dateValue), (int) h,
        (int) m, (int) s, (int) millis);
    long ms = cal.getTimeInMillis();
    Timestamp x = new Timestamp(ms);
    x.setNanos((int) (nanos + millis * 1000000));
    return x;
  }

  /**
   * Convert the date using the specified calendar.
   * 
   * @param x
   *          the date
   * @param calendar
   *          the calendar
   * @return the date
   */
  public static ValueDate convertDate(Date x, Calendar calendar) {
    if (calendar == null) {
      throw JdbcException.getInvalidValueException("calendar", null);
    }
    Calendar cal = (Calendar) calendar.clone();
    cal.setTimeInMillis(x.getTime());
    long dateValue = dateValueFromCalendar(cal);
    return ValueDate.fromDateValue(dateValue);
  }

  /**
   * Convert the time using the specified calendar.
   * 
   * @param x
   *          the time
   * @param calendar
   *          the calendar
   * @return the time
   */
  public static ValueTime convertTime(Time x, Calendar calendar) {
    if (calendar == null) {
      throw JdbcException.getInvalidValueException("calendar", null);
    }
    Calendar cal = (Calendar) calendar.clone();
    cal.setTimeInMillis(x.getTime());
    long nanos = nanosFromCalendar(cal);
    return ValueTime.fromNanos(nanos);
  }

  /**
   * Convert a date to the specified time zone.
   * 
   * @param x
   *          the date to convert
   * @param target
   *          the calendar with the target timezone
   * @return the milliseconds the milliseconds in UTC
   */
  public static long convertToLocal(java.util.Date x, Calendar target) {
    if (target == null) {
      throw JdbcException.getInvalidValueException("calendar", null);
    }
    target = (Calendar) target.clone();
    Calendar local = Calendar.getInstance();
    synchronized (local) {
      local.setTime(x);
      convertTime(local, target);
    }
    return target.getTime().getTime();
  }

  private static void convertTime(Calendar from, Calendar to) {
    to.set(Calendar.ERA, from.get(Calendar.ERA));
    to.set(Calendar.YEAR, from.get(Calendar.YEAR));
    to.set(Calendar.MONTH, from.get(Calendar.MONTH));
    to.set(Calendar.DAY_OF_MONTH, from.get(Calendar.DAY_OF_MONTH));
    to.set(Calendar.HOUR_OF_DAY, from.get(Calendar.HOUR_OF_DAY));
    to.set(Calendar.MINUTE, from.get(Calendar.MINUTE));
    to.set(Calendar.SECOND, from.get(Calendar.SECOND));
    to.set(Calendar.MILLISECOND, from.get(Calendar.MILLISECOND));
  }

  /**
   * Convert the timestamp using the specified calendar.
   * 
   * @param x
   *          the time
   * @param calendar
   *          the calendar
   * @return the timestamp
   */
  public static ValueTimestamp convertTimestamp(Timestamp x, Calendar calendar) {
    if (calendar == null) {
      throw JdbcException.getInvalidValueException("calendar", null);
    }
    Calendar cal = (Calendar) calendar.clone();
    cal.setTimeInMillis(x.getTime());
    long dateValue = dateValueFromCalendar(cal);
    long nanos = nanosFromCalendar(cal);
    nanos += x.getNanos() % 1000000;
    return ValueTimestamp.fromDateValueAndNanos(dateValue, nanos);
  }

  /**
   * Parse a date string. The format is: [+|-]year-month-day
   * 
   * @param s
   *          the string to parse
   * @param start
   *          the parse index start
   * @param end
   *          the parse index end
   * @return the date value
   * @throws IllegalArgumentException
   *           if there is a problem
   */
  public static long parseDateValue(String s, int start, int end) {
    if (s.charAt(start) == '+') {
      // +year
      start++;
    }
    // start at position 1 to support "-year"
    int s1 = s.indexOf('-', start + 1);
    int s2 = s.indexOf('-', s1 + 1);
    if (s1 <= 0 || s2 <= s1) {
      throw new IllegalArgumentException(s);
    }
    int year = Integer.parseInt(s.substring(start, s1));
    int month = Integer.parseInt(s.substring(s1 + 1, s2));
    int day = Integer.parseInt(s.substring(s2 + 1, end));
    if (!isValidDate(year, month, day)) {
      throw new IllegalArgumentException(year + "-" + month + "-" + day);
    }
    return dateValue(year, month, day);
  }

  /**
   * Parse a time string. The format is: [-]hour:minute:second[.nanos]
   * 
   * @param s
   *          the string to parse
   * @param start
   *          the parse index start
   * @param end
   *          the parse index end
   * @param timeOfDay
   *          whether the result need to be within 0 (inclusive) and 1 day
   *          (exclusive)
   * @return the time in nanoseconds
   * @throws IllegalArgumentException
   *           if there is a problem
   */
  public static long parseTimeNanos(String s, int start, int end,
      boolean timeOfDay) {
    int hour = 0, minute = 0, second = 0;
    long nanos = 0;
    int s1 = s.indexOf(':', start);
    int s2 = s.indexOf(':', s1 + 1);
    int s3 = s.indexOf('.', s2 + 1);
    if (s1 <= 0 || s2 <= s1) {
      throw new IllegalArgumentException(s);
    }
    boolean negative;
    hour = Integer.parseInt(s.substring(start, s1));
    if (hour < 0) {
      if (timeOfDay) {
        throw new IllegalArgumentException(s);
      }
      negative = true;
      hour = -hour;
    } else {
      negative = false;
    }
    minute = Integer.parseInt(s.substring(s1 + 1, s2));
    if (s3 < 0) {
      second = Integer.parseInt(s.substring(s2 + 1, end));
    } else {
      second = Integer.parseInt(s.substring(s2 + 1, s3));
      String n = (s.substring(s3 + 1, end) + "000000000").substring(0, 9);
      nanos = Integer.parseInt(n);
    }
    if (hour >= 2000000 || minute < 0 || minute >= 60 || second < 0
        || second >= 60) {
      throw new IllegalArgumentException(s);
    }
    if (timeOfDay && hour >= 24) {
      throw new IllegalArgumentException(s);
    }
    nanos += ((((hour * 60L) + minute) * 60) + second) * 1000000000;
    return negative ? -nanos : nanos;
  }

  /**
   * Calculate the milliseconds since 1970-01-01 (UTC) for the given date and
   * time (in the specified timezone).
   * 
   * @param tz
   *          the timezone of the parameters
   * @param year
   *          the absolute year (positive or negative)
   * @param month
   *          the month (1-12)
   * @param day
   *          the day (1-31)
   * @param hour
   *          the hour (0-23)
   * @param minute
   *          the minutes (0-59)
   * @param second
   *          the number of seconds (0-59)
   * @param millis
   *          the number of milliseconds
   * @return the number of milliseconds (UTC)
   */
  public static long getMillis(TimeZone tz, int year, int month, int day,
      int hour, int minute, int second, int millis) {
    try {
      return getTimeTry(false, tz, year, month, day, hour, minute, second,
          millis);
    } catch (IllegalArgumentException e) {
      // special case: if the time simply doesn't exist because of
      // daylight saving time changes, use the lenient version
      String message = e.toString();
      if (message.indexOf("HOUR_OF_DAY") > 0) {
        if (hour < 0 || hour > 23) {
          throw e;
        }
        return getTimeTry(true, tz, year, month, day, hour, minute, second,
            millis);
      } else if (message.indexOf("DAY_OF_MONTH") > 0) {
        int maxDay;
        if (month == 2) {
          maxDay = new GregorianCalendar().isLeapYear(year) ? 29 : 28;
        } else {
          maxDay = 30 + ((month + (month > 7 ? 1 : 0)) & 1);
        }
        if (day < 1 || day > maxDay) {
          throw e;
        }
        // DAY_OF_MONTH is thrown for years > 2037
        // using the timezone Brasilia and others,
        // for example for 2042-10-12 00:00:00.
        hour += 6;
        return getTimeTry(true, tz, year, month, day, hour, minute, second,
            millis);
      } else {
        return getTimeTry(true, tz, year, month, day, hour, minute, second,
            millis);
      }
    }
  }

  private static long getTimeTry(boolean lenient, TimeZone tz, int year,
      int month, int day, int hour, int minute, int second, int millis) {
    Calendar c;
    if (tz == null) {
      c = getCalendar();
    } else {
      c = Calendar.getInstance(tz);
    }
    synchronized (c) {
      c.clear();
      c.setLenient(lenient);
      setCalendarFields(c, year, month, day, hour, minute, second, millis);
      return c.getTime().getTime();
    }
  }

  private static void setCalendarFields(Calendar cal, int year, int month,
      int day, int hour, int minute, int second, int millis) {
    if (year <= 0) {
      cal.set(Calendar.ERA, GregorianCalendar.BC);
      cal.set(Calendar.YEAR, 1 - year);
    } else {
      cal.set(Calendar.ERA, GregorianCalendar.AD);
      cal.set(Calendar.YEAR, year);
    }
    // january is 0
    cal.set(Calendar.MONTH, month - 1);
    cal.set(Calendar.DAY_OF_MONTH, day);
    cal.set(Calendar.HOUR_OF_DAY, hour);
    cal.set(Calendar.MINUTE, minute);
    cal.set(Calendar.SECOND, second);
    cal.set(Calendar.MILLISECOND, millis);
  }

  /**
   * Get the specified field of a date, however with years normalized to
   * positive or negative, and month starting with 1.
   * 
   * @param d
   *          the date
   * @param field
   *          the field type
   * @return the value
   */
  public static int getDatePart(java.util.Date d, int field) {
    Calendar c = getCalendar();
    synchronized (c) {
      c.setTime(d);
      if (field == Calendar.YEAR) {
        return getYear(c);
      }
      int value = c.get(field);
      if (field == Calendar.MONTH) {
        return value + 1;
      }
      return value;
    }
  }

  /**
   * Get the year (positive or negative) from a calendar.
   * 
   * @param calendar
   *          the calendar
   * @return the year
   */
  private static int getYear(Calendar calendar) {
    int year = calendar.get(Calendar.YEAR);
    if (calendar.get(Calendar.ERA) == GregorianCalendar.BC) {
      year = 1 - year;
    }
    return year;
  }

  /**
   * Get the number of milliseconds since 1970-01-01 in the local timezone, but
   * without daylight saving time into account.
   * 
   * @param d
   *          the date
   * @return the milliseconds
   */
  public static long getTimeLocalWithoutDst(java.util.Date d) {
    return d.getTime() + zoneOffset;
  }

  /**
   * Convert the number of milliseconds since 1970-01-01 in the local timezone
   * to UTC, but without daylight saving time into account.
   * 
   * @param millis
   *          the number of milliseconds in the local timezone
   * @return the number of milliseconds in UTC
   */
  public static long getTimeUTCWithoutDst(long millis) {
    return millis - zoneOffset;
  }

  /**
   * Return the day of week according to the ISO 8601 specification. Week starts
   * at Monday. See also http://en.wikipedia.org/wiki/ISO_8601
   * 
   * @author Robert Rathsack
   * 
   * @param date
   *          the date object which day of week should be calculated
   * @return the day of the week, Monday as 1 to Sunday as 7
   */
  public static int getIsoDayOfWeek(java.util.Date date) {
    Calendar cal = Calendar.getInstance();
    cal.setTimeInMillis(date.getTime());
    int val = cal.get(Calendar.DAY_OF_WEEK) - 1;
    return val == 0 ? 7 : val;
  }

  /**
   * Returns the week of the year according to the ISO 8601 specification. The
   * spec defines the first week of the year as the week which contains at least
   * 4 days of the new year. The week starts at Monday. Therefore December 29th
   * - 31th could belong to the next year and January 1st - 3th could belong to
   * the previous year. If January 1st is on Thursday (or earlier) it belongs to
   * the first week, otherwise to the last week of the previous year. Hence
   * January 4th always belongs to the first week while the December 28th always
   * belongs to the last week.
   * 
   * @author Robert Rathsack
   * @param date
   *          the date object which week of year should be calculated
   * @return the week of the year
   */
  public static int getIsoWeek(java.util.Date date) {
    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(date.getTime());
    c.setFirstDayOfWeek(Calendar.MONDAY);
    c.setMinimalDaysInFirstWeek(4);
    return c.get(Calendar.WEEK_OF_YEAR);
  }

  /**
   * Returns the year according to the ISO week definition.
   * 
   * @author Robert Rathsack
   * 
   * @param date
   *          the date object which year should be calculated
   * @return the year
   */
  public static int getIsoYear(java.util.Date date) {
    Calendar cal = Calendar.getInstance();
    cal.setTimeInMillis(date.getTime());
    cal.setFirstDayOfWeek(Calendar.MONDAY);
    cal.setMinimalDaysInFirstWeek(4);
    int year = getYear(cal);
    int month = cal.get(Calendar.MONTH);
    int week = cal.get(Calendar.WEEK_OF_YEAR);
    if (month == 0 && week > 51) {
      year--;
    } else if (month == 11 && week == 1) {
      year++;
    }
    return year;
  }

  /**
   * Formats a date using a format string.
   * 
   * @param date
   *          the date to format
   * @param format
   *          the format string
   * @param locale
   *          the locale
   * @param timeZone
   *          the timezone
   * @return the formatted date
   */
  public static String formatDateTime(java.util.Date date, String format,
      String locale, String timeZone) {
    SimpleDateFormat dateFormat = getDateFormat(format, locale, timeZone);
    synchronized (dateFormat) {
      return dateFormat.format(date);
    }
  }

  /**
   * Parses a date using a format string.
   * 
   * @param date
   *          the date to parse
   * @param format
   *          the parsing format
   * @param locale
   *          the locale
   * @param timeZone
   *          the timeZone
   * @return the parsed date
   */
  public static java.util.Date parseDateTime(String date, String format,
      String locale, String timeZone) {
    SimpleDateFormat dateFormat = getDateFormat(format, locale, timeZone);
    try {
      synchronized (dateFormat) {
        return dateFormat.parse(date);
      }
    } catch (Exception e) {
      // ParseException
      throw JdbcException.get(SQLErrorCode.PARSE_ERROR_1, e, date);
    }
  }

  private static SimpleDateFormat getDateFormat(String format, String locale,
      String timeZone) {
    try {
      // currently, a new instance is create for each call
      // however, could cache the last few instances
      SimpleDateFormat df;
      if (locale == null) {
        df = new SimpleDateFormat(format);
      } else {
        Locale l = new Locale(locale);
        df = new SimpleDateFormat(format, l);
      }
      if (timeZone != null) {
        df.setTimeZone(TimeZone.getTimeZone(timeZone));
      }
      return df;
    } catch (Exception e) {
      throw JdbcException.get(SQLErrorCode.PARSE_ERROR_1, e, format + "/" + locale
          + "/" + timeZone);
    }
  }

  /**
   * Verify if the specified date is valid.
   * 
   * @param year
   *          the year
   * @param month
   *          the month (January is 1)
   * @param day
   *          the day (1 is the first of the month)
   * @return true if it is valid
   */
  public static boolean isValidDate(int year, int month, int day) {
    if (month < 1 || month > 12 || day < 1) {
      return false;
    }
    if (year > 1582) {
      // Gregorian calendar
      if (month != 2) {
        return day <= NORMAL_DAYS_PER_MONTH[month];
      }
      // February
      if ((year & 3) != 0) {
        return day <= 28;
      }
      return day <= ((year % 100 != 0) || (year % 400 == 0) ? 29 : 28);
    } else if (year == 1582 && month == 10) {
      // special case: days 1582-10-05 .. 1582-10-14 don't exist
      return day <= 31 && (day < 5 || day > 14);
    }
    if (month != 2 && day <= NORMAL_DAYS_PER_MONTH[month]) {
      return true;
    }
    return day <= ((year & 3) != 0 ? 28 : 29);
  }

  /**
   * Convert a date value to a date, using the default timezone.
   * 
   * @param dateValue
   *          the date value
   * @return the date
   */
  public static Date convertDateValueToDate(long dateValue) {
    long millis = getMillis(TimeZone.getDefault(),
        yearFromDateValue(dateValue), monthFromDateValue(dateValue),
        dayFromDateValue(dateValue), 0, 0, 0, 0);
    return new Date(millis);
  }

  /**
   * Convert a date value / time value to a timestamp, using the default
   * timezone.
   * 
   * @param dateValue
   *          the date value
   * @param nanos
   *          the nanoseconds since midnight
   * @return the timestamp
   */
  public static Timestamp convertDateValueToTimestamp(long dateValue, long nanos) {
    long millis = nanos / 1000000;
    nanos -= millis * 1000000;
    long s = millis / 1000;
    millis -= s * 1000;
    long m = s / 60;
    s -= m * 60;
    long h = m / 60;
    m -= h * 60;
    long ms = getMillis(TimeZone.getDefault(), yearFromDateValue(dateValue),
        monthFromDateValue(dateValue), dayFromDateValue(dateValue), (int) h,
        (int) m, (int) s, 0);
    Timestamp ts = new Timestamp(ms);
    ts.setNanos((int) (nanos + millis * 1000000));
    return ts;
  }

  /**
   * Convert a time value to a time, using the default timezone.
   * 
   * @param nanos
   *          the nanoseconds since midnight
   * @return the time
   */
  public static Time convertNanoToTime(long nanos) {
    long millis = nanos / 1000000;
    long s = millis / 1000;
    millis -= s * 1000;
    long m = s / 60;
    s -= m * 60;
    long h = m / 60;
    m -= h * 60;
    long ms = getMillis(TimeZone.getDefault(), 1970, 1, 1, (int) (h % 24),
        (int) m, (int) s, (int) millis);
    return new Time(ms);
  }

  /**
   * Get the year from a date value.
   * 
   * @param x
   *          the date value
   * @return the year
   */
  public static int yearFromDateValue(long x) {
    return (int) (x >>> SHIFT_YEAR);
  }

  /**
   * Get the month from a date value.
   * 
   * @param x
   *          the date value
   * @return the month (1..12)
   */
  public static int monthFromDateValue(long x) {
    return (int) (x >>> SHIFT_MONTH) & 15;
  }

  /**
   * Get the day of month from a date value.
   * 
   * @param x
   *          the date value
   * @return the day (1..31)
   */
  public static int dayFromDateValue(long x) {
    return (int) (x & 31);
  }

  /**
   * Get the date value from a given date.
   * 
   * @param year
   *          the year
   * @param month
   *          the month (1..12)
   * @param day
   *          the day (1..31)
   * @return the date value
   */
  public static long dateValue(long year, int month, int day) {
    return (year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day;
  }

  /**
   * Calculate the date value (in the default timezone) from a given time in
   * milliseconds in UTC.
   * 
   * @param ms
   *          the milliseconds
   * @return the date value
   */
  public static long dateValueFromDate(long ms) {
    Calendar cal = getCalendar();
    synchronized (cal) {
      cal.clear();
      cal.setTimeInMillis(ms);
      return dateValueFromCalendar(cal);
    }
  }

  /**
   * Calculate the date value from a given calendar.
   * 
   * @param cal
   *          the calendar
   * @return the date value
   */
  private static long dateValueFromCalendar(Calendar cal) {
    int year, month, day;
    year = getYear(cal);
    month = cal.get(Calendar.MONTH) + 1;
    day = cal.get(Calendar.DAY_OF_MONTH);
    return ((long) year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day;
  }

  /**
   * Calculate the nanoseconds since midnight (in the default timezone) from a
   * given time in milliseconds in UTC.
   * 
   * @param ms
   *          the milliseconds
   * @return the nanoseconds
   */
  public static long nanosFromDate(long ms) {
    Calendar cal = getCalendar();
    synchronized (cal) {
      cal.clear();
      cal.setTimeInMillis(ms);
      return nanosFromCalendar(cal);
    }
  }

  /**
   * Calculate the nanoseconds since midnight from a given calendar.
   * 
   * @param cal
   *          the calendar
   * @return the nanoseconds
   */
  private static long nanosFromCalendar(Calendar cal) {
    int h = cal.get(Calendar.HOUR_OF_DAY);
    int m = cal.get(Calendar.MINUTE);
    int s = cal.get(Calendar.SECOND);
    int millis = cal.get(Calendar.MILLISECOND);
    return ((((((h * 60L) + m) * 60) + s) * 1000) + millis) * 1000000;
  }

  /**
   * Calculate the normalized timestamp.
   * 
   * @param absoluteDay
   *          the absolute day
   * @param nanos
   *          the nanoseconds (may be negative or larger than one day)
   * @return the timestamp
   */
  public static ValueTimestamp normalizeTimestamp(long absoluteDay, long nanos) {
    if (nanos > NANOS_PER_DAY || nanos < 0) {
      long d;
      if (nanos > NANOS_PER_DAY) {
        d = nanos / NANOS_PER_DAY;
      } else {
        d = (nanos - NANOS_PER_DAY + 1) / NANOS_PER_DAY;
      }
      nanos -= d * NANOS_PER_DAY;
      absoluteDay += d;
    }
    return ValueTimestamp.fromDateValueAndNanos(
        dateValueFromAbsoluteDay(absoluteDay), nanos);
  }

  /**
   * Calculate the absolute day from a date value.
   * 
   * @param dateValue
   *          the date value
   * @return the absolute day
   */
  public static long absoluteDayFromDateValue(long dateValue) {
    long y = yearFromDateValue(dateValue);
    int m = monthFromDateValue(dateValue);
    int d = dayFromDateValue(dateValue);
    if (m <= 2) {
      y--;
      m += 12;
    }
    long a = ((y * 2922L) >> 3) + DAYS_OFFSET[m - 3] + d - 719484;
    if (y <= 1582 && ((y < 1582) || (m * 100 + d < 1005))) {
      // Julian calendar (cutover at 1582-10-04 / 1582-10-15)
      a += 13;
    } else if (y < 1901 || y > 2099) {
      // Gregorian calendar (slow mode)
      a += (y / 400) - (y / 100) + 15;
    }
    return a;
  }

  /**
   * Calculate the date value from an absolute day.
   * 
   * @param absoluteDay
   *          the absolute day
   * @return the date value
   */
  public static long dateValueFromAbsoluteDay(long absoluteDay) {
    long d = absoluteDay + 719468;
    long y100 = 0, offset;
    if (d > 578040) {
      // Gregorian calendar
      long y400 = d / 146097;
      d -= y400 * 146097;
      y100 = d / 36524;
      d -= y100 * 36524;
      offset = y400 * 400 + y100 * 100;
    } else {
      // Julian calendar
      d += 292200000002L;
      offset = -800000000;
    }
    long y4 = d / 1461;
    d -= y4 * 1461;
    long y = d / 365;
    d -= y * 365;
    if (d == 0 && (y == 4 || y100 == 4)) {
      y--;
      d += 365;
    }
    y += offset + y4 * 4;
    // month of a day
    int m = ((int) d * 2 + 1) * 5 / 306;
    d -= DAYS_OFFSET[m] - 1;
    if (m >= 10) {
      y++;
      m -= 12;
    }
    return dateValue(y, m + 3, (int) d);
  }

}
